概述
在 flask 中实现文件上传, 需要使用的是 request.files
对象, 基本流程是:
- 在模板的定义
enctype=multipart/form-data
的form
- 在
form
中定义input[type=file, name=filename]
- 使用
request.files[filename]
来获取上传的文件FileStorage
对象 FileStorage
对象中,filename
为文件名, 使用read
方法可以读取文件内容
在 flask 中实现文件下载, 需要使用 send_file
方法, 基本流程是:
- 构造/获取 需要被下载文件的
file pointer
- 以
send_file(fp, as_attachment=True, attachment_filename=filename)
作为返回响应
import tempfilefrom flask
import requests, send_file
def handler(content_raw):
# handle raw content in file
return content_raw
def read_and_download():
file = request.files.values()[0]
filename, file_content = file.filename, file.read()
content_handled = handler(file_content)
# handler use to handle file content
tmp = tempfile.TemporaryFile()
# construct a tmp file
tmp.write(content_handled)
# save handled content to tmp file
tmp.seek(0)
# reset file stream position
return send_file(tmp, as_attachment=True, attachment_filename="download.ext")
文件上传的原理
文件上传, 其本质是通过客户端(仅以浏览器为例)将一种数据传输到服务器(即通过 form 提交数据), 其特殊之处在于, 传输的数据类型是二进制类型数据
一般的情况下, 我们通过 form 提交数据, 要做的事情有两件:
- 定义提交的目的地, 通过指定 form 的 action 实现
- 定义提交的方法, 通过指定 form 的 method 实现
当服务器接收到来自浏览器的请求之后, 会根据请求头的内容对数据进行处理.
对于一般的 form 数据, 浏览器会默认为请求头添加 Content-Type: application/x-www-form-urlencoded
(其含义为传输的数据类型是经过 URL 编码的数据).
但是对于文件类型的数据我们必须要手动设置 Content-Type
(因为文件的类型是二进制数据), 因此, 对于文件上传, 我们需要针对提交 form 数据添加额外的参数:
- 设置
form
的enctype
为multipart/form-data
设置之后, 请求头中会出现 Content-Type: multipart/form-data
, multipart/form-data
这一类型的含义是被分割为多份的表单数据.
通过这样的处理, 文件就能被服务器接收到并且进行正确的处理, 下面是一个基于 flask 的文件上传的请求头实例
在 flask 中, 对于 上传的文件数据 的获取主要是在 werkzeug
中实现的, 想要阅读源码(flask==1.0.2)可以参考以下路径:
- flask/wrappers.py[L:168] -
Request._load_form_data
- werkzeug/wrappers.py[L:379 ~ L:385] -
parser.parse
- werkzeug/formparser.py[L:213] -
_parse_multipart
文件下载的原理
这里所说的文件下载并不是只将 flask 作为 client 进行下载, 而是指访问者可以从 flask server 下载文件
其实, 文件下载的本质是客户端(以浏览器为例)针对服务器响应的特殊处理.
我们看到的网页内容本质上都是服务器返回给浏览器的数据
, 这些数据之所以会以不同的形式出现, 是因为浏览器会根据响应头中的 Content-Type
决定采用何种方式处理这些数据
.
例如, 如果在响应头中存在 Content-Type: text/html
, 那么浏览器就会将返回数据以 HTML 的形式进行渲染.
那么一个可以下载文件的响应的请求头回事什么样的呢?
上图是一个简单的基于 flask 实现的文件下载的响应头, 图中红框标注的是要重点关注的地方.
Content-Type: application/octet-stream
表示响应数据的类型是 通用的二进制
类型数据, 浏览器针对 通用二进制
类型数据的默认处理方法就是触发 下载
操作.
Content-Disposition: attachment; filename="filename.ext"
的含义是, 以附件形式下载, 并且下载文件名为 filename.ext.
设置 Content-Disposition
头的原因有二:
- 某些类型的二进制数据浏览器是可用进行渲染的(比如 json 或者 pdf), 因此浏览器的默认行为就不再是下载而是渲染(此时
Content-Disposition
的值为inline
, 因此对于如果希望总是触发下载操作, 就需要手动设置Content-Disposition
为attachment
(即下载). - 默认的情况下, 浏览器触发的下载操作并不会对下载的文件进行命名, 因此需要手动进行命名, 即为
Content-Disposition
添加filename="filename"
.
Flask 中实现文件上传和下载的方法
在 flask 中实现文件的上传与下载并不复杂, 因为许多针对底层的操作 flask 已经封装好了.
实现文件上传
flask 中实现文件上传的步骤主要有四:
- 在 template 中定义
enctype
为multipart/form-data
,method
为POST
的 form 元素 - 在 form 添加
input[type=file]
元素 - 在 views 中定义处理 form action 的方法
- 在 view function 中 使用
request.files
获取文件并进行处理
下面是一个简单的例子(摘自 flask 官方文档并进行了一定的修改, 直接保存为 app.py 并运行即可)
from flask import request, Flask, redirect
def upload_file():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
print('No file part')
return redirect(request.url)
file = request.files['file']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
print('No selected file')
return redirect(request.url)
return file.read().decode() # convert to str
return '''
<!doctype html>人
<title>Upload new File</title>
<h1>Upload new File</h1>
<form action="{{ url_for('upload') }}" method="POST" enctype=multipart/form-data>
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
</html>
'''
app = Flask(__file__)
app.add_url_rule('', 'index', upload_file, methods=('GET',))
app.add_url_rule('', 'download', upload_file, methods=('POST',))
app.run(debug=True)
flask.requests
是一个 MultiDict
(即一个 key 可以对应多个 value), 通过在定义 input[type=file, name=name]
即可以通过 flask.requests[name]
的方式获取到上传的文件.
上传的文件是 FileStorage 的实例, FileStorage
是 werkzeug
针对 request stream
的封装, 所以可以直接使用 request stream
的方法进行处理.
比如, 获取文件名: FileStorage().filename
, 获取文件大小: FileStorage().content_length
, 获取文件内容: FileStorage().read()
注意
FileStorage
是stream
的封装, 因此在read
之后 pointer 就指向了stream
的末尾, 此时再次read
无法得到任何内容, 如果需要再次使用read
方法获取内容, 需要先 file.seek(0) 将 pointer 重新指向stream
头部
实现文件下载
实现文件下载主要是利用 flask 提供的 send_file
, 一个简单的例子如下(直接保存为 app.py 并运行即可):
import json
import tempfile
from flask import Flask, send_file
def download_file():
file = tempfile.TemporaryFile()
json.dump({'name': 'demo', 'age': 22}, file)
return send_file(file, as_attachment=True, attachment_filename='demo.json')
app = Flask(__file__)
app.add_url_rule('', 'download', download_file, methods=('GET,'))
app.run(debug=True)
如过希望下载文件, 则必须设置 as_attachment=True
以及 attachment_filename=filename
.
需要注意的是, send_file
的第一个参数要求必须是 file pointer
(后续简称 fp). 一般地, 在 python 中得到一个 fp 的方式是使用 open
方法打开一个文件, 但这就要求文件必须存储于服务器上, 在某些情况下这样子是可行的, 但是在一般情况下, 服务器并不需要保存生成的文件, 在文件被下载之后就可以移除, 这样子能减少服务器在存储上的负担.
为此, 可以使用临时文件解决这个问题, 使用 python 标准库中的 tempfile
可以很方便地生成临时文件, 如果使用 (Named)TemporaryFile
甚至可以不考虑清除临时文件的问题(tempfile
会在文件被关闭后自行清理).
注意:
-
不要以
with
context(如 with tempfile.Temporary() as wf: …) 的形式使用tempfile
, 因为在这种情况下执行file.write
(或类似操作)之后会自动关闭 fp 导致send_file
时遇到 的错误(有一点我无法确认, 在 flasksend_file
完成之后是否会自动 close file)send closed file
-
当执行完
file.write
(或类似操作)之后, 需要使用file.seek(0)
重置stream
位置(原因在上一节最后有说明), 否则下载的文件将会是空文件
实例
Online File Converter 是一个基于 flask 实现的站点, 这个站点的作用是将用户上传的 csv
文件合并后以 excel
的形式下载, 这是一个关于 flask 文件上传和下载的完整例子.